昨日在 riscv-go 上面的 riscvdev 分支裡挖掘到了重定向型態短少的原因,也從隔壁的 riscv-binutils-gdb 專案取得了正確的對照。今天的內容就先來了解這些重定在 ELF 檔中的樣子吧!
$ riscv64-unknown-linux-gnu-readelf -r ~/main.o
Relocation section '.rela.text' at offset 0x1b8 contains 9 entries:
Offset Info Type Sym. Value Sym. Name + Addend
000000000000 00000000002b R_RISCV_ALIGN 0
00000000000c 000900000012 R_RISCV_CALL 0000000000000000 add + 0
00000000000c 000000000033 R_RISCV_RELAX 0
000000000018 00060000001a R_RISCV_HI20 0000000000000000 .LC0 + 0
000000000018 000000000033 R_RISCV_RELAX 0
00000000001c 00060000001b R_RISCV_LO12_I 0000000000000000 .LC0 + 0
00000000001c 000000000033 R_RISCV_RELAX 0
000000000020 000a00000012 R_RISCV_CALL 0000000000000000 printf + 0
000000000020 000000000033 R_RISCV_RELAX 0
理論上,重定型態的展示應該也寫在 API 裡面,從線上的官方文件裡的 PPC(IBM 的架構)為例:
type R_PPC int
const (
R_PPC_NONE R_PPC = 0 /* No relocation. */
R_PPC_ADDR32 R_PPC = 1
R_PPC_ADDR24 R_PPC = 2
R_PPC_ADDR16 R_PPC = 3
...
{115, "R_PPC_EMB_BIT_FLD"},
{116, "R_PPC_EMB_RELSDA"},
}
func (i R_PPC) String() string { return stringName(uint32(i), rppcStrings, false) }
func (i R_PPC) GoString() string { return stringName(uint32(i), rppcStrings, true) }
就跟我們之前在實作 readelf 的時候一樣,我們曾經使用過這些 API。而幸好,雖然他們只有移植 12 種動態連結必須的重定型態,這些 API 是沒有少的。也就是說,我們仍然可以仿照 nm 實作標籤區段的展示那樣,為 readelf 新增 -r
參數來展示重定區段;然而我們預期的行為是,因為實際上缺乏這些重定,因此進化過的我們的 readelf 應該會沒有辦法正確地顯示,嚴重的話甚至可能會在存取字串索引時出現問題。
我們已經聊重定型態、重定在連結中的意義很多次了。但它們在 ELF 檔裡面到底是什麼東西?開始一探究竟之前,我們可以先看看 main.o 物件檔的f區段檔頭資訊:
Section Header:
Number Name Type Flags Address Offset Size Link Info Alignment
0 .shstrtab elf.SHT_STRTAB 0x0 0 290 66 0 0 0x1
1 .strtab elf.SHT_STRTAB 0x0 0 198 29 0 0 0x1
2 .symtab elf.SHT_SYMTAB 0x0 0 90 264 8 8 0x8
3 .comment elf.SHT_PROGBITS elf.SHF_MERGE+elf.SHF_STRINGS 0 7c 18 0 0 0x1
4 .rodata elf.SHT_PROGBITS elf.SHF_ALLOC 0 78 4 0 0 0x8
5 .bss elf.SHT_NOBITS elf.SHF_WRITE+elf.SHF_ALLOC 0 74 0 0 0 0x1
6 .data elf.SHT_PROGBITS elf.SHF_WRITE+elf.SHF_ALLOC 0 74 0 0 0 0x1
7 .rela.text elf.SHT_RELA elf.SHF_INFO_LINK 0 1b8 216 7 1 0x8
8 .text elf.SHT_PROGBITS elf.SHF_ALLOC+elf.SHF_EXECINSTR 0 40 52 0 0 0x2
9 elf.SHT_NULL 0x0 0 0 0 0 0 0x0
有一個我們之前沒看過的區段,它想必就是存放連結所需資訊的那個區段了:.rela.text
。.text
我們可以理解是程式碼內容的代表,但是 .rela
卻是什麼?我們可以從 elf 函式庫的結構定義來理解這一點:
/* ELF64 relocations that don't need an addend field. */
type Rel64 struct {
Off uint64 /* Location to be relocated. */
Info uint64 /* Relocation type and symbol index. */
}
/* ELF64 relocations that need an addend field. */
type Rela64 struct {
Off uint64 /* Location to be relocated. */
Info uint64 /* Relocation type and symbol index. */
Addend int64 /* Addend. */
}
這裡有兩個兄弟結構,唯一的差別是 Rela
有多一個成員。Off
代表需要重定的位址,而 Info
又再度是一個複合欄位,分別用來代表重定型態和相關連的標籤的索引號碼。如果有需要那個 Addend
(筆者姑且譯作補正量)資訊,那麼連結器在解決重定區域的時候就會使用到。這一點我們節錄一下 RISC-V 的 PS-ABI 文件:
Enum | ELF Reloc Type | Description | Details |
---|---|---|---|
0 | R_RISCV_NONE | None | |
1 | R_RISCV_32 | Runtime relocation | word32 = S + A |
2 | R_RISCV_64 | Runtime relocation | word64 = S + A |
3 | R_RISCV_RELATIVE | Runtime relocation | word32,64 = B + A |
... | ... | ... | ... |
其中後附說明中, A
就是這個修正量的意義。那麼其他的呢?也在同一份文件之中:S
代表的是該指定 symbol 的位址,B
則是代表動態連結函式庫在載入記憶體裡面的時候的基底。
回頭再仔細看這個 .rela.text
區段的展示:
Section Header:
Number Name Type Flags Address Offset Size Link Info Alignment
...
7 .rela.text elf.SHT_RELA elf.SHF_INFO_LINK 0 1b8 216 7 1 0x8
看到區段型態是 SHT_RELA
,就不必多說了吧。這個區段旗標的 SHF_INFO_LINK
則是表示說,在這個區段的 Info
欄位中,存放的重定所在的區域。Link
欄位向來是代表該區段要參考到的區段,在這個例子,也就是之前為了實作 nm 有好好摸一下的 .symtab
區段啦。但是眼尖的讀者應該有發現,明明 .text
區段就是 8 而不是 1、.symtab
區段是 2 而不是 7,那麼這裡怎麼會變成這樣呢?其實是因為我們的 readelf 透過 go 語言的讀取,會把原先的順序倒轉過來的緣故,所以有一個 mapping。
當然,這是日後必須處理的問題。
Size
是 9?沒錯。Rela64
結構包含 3 個 uint64
整數,所以是 24 個 bytes;總共有 9 個重定被安插在 main.o 中,所以總共是 216 位元組無誤。對齊量是 8 bytes,所以我們不用考慮其他修修補補的問題。
使用二進位檔展示工具 hexdump,尋找 1b8 的區段:
000001b0 00 00 00 00 00 00 00 00 | ........|
000001c0 2b 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |+...............|
000001d0 0c 00 00 00 00 00 00 00 12 00 00 00 09 00 00 00 |................|
000001e0 00 00 00 00 00 00 00 00 0c 00 00 00 00 00 00 00 |................|
000001f0 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |3...............|
00000200 18 00 00 00 00 00 00 00 1a 00 00 00 06 00 00 00 |................|
00000210 00 00 00 00 00 00 00 00 18 00 00 00 00 00 00 00 |................|
00000220 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |3...............|
00000230 1c 00 00 00 00 00 00 00 1b 00 00 00 06 00 00 00 |................|
00000240 00 00 00 00 00 00 00 00 1c 00 00 00 00 00 00 00 |................|
00000250 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |3...............|
00000260 20 00 00 00 00 00 00 00 12 00 00 00 0a 00 00 00 | ...............|
00000270 00 00 00 00 00 00 00 00 20 00 00 00 00 00 00 00 |........ .......|
00000280 33 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |3...............|
未求方便閱讀,筆者除了第一組重定資訊,其他的兩個兩個一組,因為 24 bytes 相當於是 1.5 行。再複習一下 Rela64
的結構:
type Rela64 struct {
Off uint64 /* Location to be relocated. */
Info uint64 /* Relocation type and symbol index. */
Addend int64 /* Addend. */
}
而這個 Info
成員的組合方式是:
func R_SYM64(info uint64) uint32 { return uint32(info >> 32) }
func R_TYPE64(info uint64) uint32 { return uint32(info) }
func R_INFO(sym, typ uint32) uint64 { return uint64(sym)<<32 | uint64(typ) }
也就是說,剛好切成一半的意思啦!比較高位的一半是(因為小頭排序的緣故,所以是右半)對應到的標籤,而比較低位的一半則是型態。我們來隨意檢驗一下,比方說位在 278 的最後一組重定結構,第一個成員 Off
的內容是 0x20,沒有指定 symbol,重定型態是編號 0x33,也就是 51,這個東西剛好就是 R_RISCV_RELAX
的重定型態;他的前一個,則是型態編號為 0x12 的 R_RISCV_CALL
,應該是呼叫到 printf
函式的那一組吧?檢查一下反組譯內容:
0000000000000000 <main>:
...
20: 00000097 auipc ra,0x0
20: R_RISCV_CALL printf
20: R_RISCV_RELAX *ABS*
果然如此吧!
今日我們看了 Rela 的結構型態,並且理解它在 ELF 檔之中的角色。各位讀者天冷請不要著涼啦!我們明日再會!